writing code well · quotes and characters · indexing and slicing · methods · looping over strings · f-strings
A program can produce the right output and still be badly written. Style is everything about how code looks and is organized, separate from whether it runs correctly. Good style avoids bugs, saves time, saves money, and makes you more employable, so it's graded here just like correctness is. A technically correct program with poor style should not earn full marks.
i, j, k, and n, x, y are fine for indexes and plain numbers with no better name.# i is a standard index name,
# so this comment is unnecessary
for i in range(len(roster)):
checkAttendance(roster[i])
-1, 0, 1, 2, and 10 are common enough to appear anywhere without a name.
def c(w):
return w*2.5+5.99
This function computes the cost of shipping a package that weighs w pounds. List at least two style-checklist violations you can find, and how you'd fix each one.
Give the function and its parameter meaningful names, and replace 2.5 and 5.99 with well-named variables. The math should stay the same: c(4) should still print 15.99.
def c(w):
return w*2.5+5.99
def shippingCost(weightInPounds):
RATE_PER_POUND = 2.5
BASE_FEE = 5.99
return weightInPounds * RATE_PER_POUND + BASE_FEE
Nothing about what the code does changed. What changed is how quickly someone else, including future you, can tell what it does.
def area1(w, h):
return w * h
def area2(base, height):
return base * height
A string represents text of any kind: a name, a sentence, the contents of a file, a web page, even a Python program's own source code. Over this section we'll cover how to write string literals, the operators and functions built for strings, the character codes underneath every string, string methods, looping over strings, and formatting values into strings with f-strings.
Name one piece of text data you interact with regularly: a username, a phone number, a search query, a song title. What's one thing a program might need to do with it: search inside it, break it into pieces, change its case, or something else?
A literal is a constant value written directly into your code, as opposed to a variable. 123, 45.6, and True are all literals. A string literal is simply a string constant, like 'Yes!'.
The literal is literally in your code.
'like this') are preferred by default."like this") work identically, and are handy when the text itself contains an apostrophe.'''like this''') both work the same way. We'll just call them "triple-quotes."print("You can't stop me!")
print('You can't stop me!')
Predict yes or no, and explain why.
Change the quoting so the line runs without crashing, and still prints exactly You can't stop me!
Spaces, newlines, and tabs are all whitespace. A single- or double-quoted literal cannot contain an actual newline (typed with the "enter" key); doing so is a syntax error. A triple-quoted literal can, and that's the main reason triple-quotes exist.
poem = '''Code flows like water,
bugs hide in the quiet lines,
patience finds them all.'''
print(poem)
It's often more readable to start the text on its own line, right after the opening triple-quote. But that puts a newline right after the quote, which shows up as a blank first line of output. When the text is meant to line up with something printed just before it, like a boxed table sitting right under its heading, that stray blank line breaks the alignment. The fix: a backslash (\, not a forward slash) right after the opening quote tells Python to ignore that one newline.
print('Order Summary:')
print('''
+-----------------+
| Coffee $8.00 |
+-----------------+''')
print('Order Summary:')
print('''\
+-----------------+
| Coffee $8.00 |
+-----------------+''')
In the misaligned version, the box floats a line below the heading it belongs with. In the fixed version, the top border lands right under Order Summary:, where it should.
Fix the code so the box's top border lands right under Order Summary:, with no blank line in between.
You can't type a real newline into a single-quoted literal, but you can write \n instead. The backslash is called the escape character, and together \n counts as one single newline character, behaving exactly like a real one.
s = "Knock knock!\nWho's there?"
print(s)
A single very long string literal is technically legal but hard to read, and breaks the 80-character line limit. The fix: split it into shorter pieces joined with +, wrapped in parentheses so Python knows the statement continues across lines.
bio = ("Margaret Hamilton led the team that wrote "
"the onboard flight software for the Apollo "
"missions, and helped define what it even "
"means to engineer software.")
print(bio)
Python's string module bundles a few useful constants: every letter, every digit, all punctuation, and all whitespace, already assembled into strings for you.
import string
print(string.ascii_lowercase) # abcd...xyz
print(string.ascii_uppercase) # ABCD...XYZ
print(string.digits) # 0123456789
Which literal correctly represents the text: She said, "Nice job!"
'She said, "Nice job!"'"She said, "Nice job!""'She said, \'Nice job!\''She said, "Nice job!" with no quotes at allprint('Row1\\nRow2')
What does this print? (There are two backslashes in a row before the n.)
Row1 and Row2 on two separate linesRow1\nRow2 on one line, backslash and n includedRowRow12nn, jumbled together\n is a single escaped newline character usable inside single- or double-quoted literals. A doubled backslash, \\n, prints as literal text instead.+ inside parentheses, to stay under 80 characters per line.Several familiar operators behave differently on strings than on numbers, and strings gain a few new operators of their own: checking whether one string contains another, and reaching in to grab individual characters or entire chunks.
If s and t are strings, s + t performs concatenation: a new string with t stuck onto the end of s. If n is an integer, s * n (or n * s) performs repetition: a new string made of n copies of s.
print('ha' * 3 + '!') # hahaha!
Line 1 finishes building s before line 2 ever runs. By the time print executes, it's just concatenating two already-known strings.
in and not in are Boolean string operators: they test whether one string occurs somewhere inside another.'' is considered to be inside every string.print('thm' in 'rhythm') # True
print('thy' in 'rhythm') # False
print('RHY' in 'rhythm') # False
print('' in 'rhythm') # True
password = 'Tr0ub4dor'
print('0' in password)
print('99' in password)
If s is a string and i is an integer, s[i] is the character at index i, counting from the left starting at 0. Negative indexes count from the right, starting at -1. A string of length n has valid indexes 0 through n-1, and -n through -1. Anything past that crashes with string index out of range.
Compare each index against the strip on the previous slide as you watch these resolve.
'H''T''O'
s[i:j] returns the part of s from index i (inclusive) to j (exclusive), just like range(). A missing start defaults to 0; a missing end defaults to the end of the string; a third value is the step.
s = 'PYTHON'
print(s[1:4]) # 'YTH'
print(s[:3]) # 'PYT'
print(s[3:]) # 'HON'
print(s[::2]) # 'PTO'
s[::-1] reverses a string, but many programmers find it unclear at a glance. Wrapping it in a small, clearly-named helper function is better style: the trick stays in one place, and every call site reads plainly.
def reverseString(s):
return s[::-1]
print(reverseString('python')) # 'nohtyp'
reverseString('python') should return 'nohtyp'. This version drops the last character with s[:-1] before reversing. Predict what it actually prints for both words, then run it.
def reverseString(s):
return s[:-1][::-1]
print(reverseString('python'))
print(reverseString('level'))
s[:-1] drops s's last character before anything is reversed. But that character was supposed to become the reversed string's first character. Dropping it means it never appears anywhere in the output. The fix is simply s[::-1], reversing the whole string in one step with nothing removed first.
s[:-1][::-1] is two separate slices, applied in order, not one combined operation. Whatever the first slice removes is gone for good before the second slice even runs.
'BDF''ABC''BDFH''ACE'+ concatenates, * repeats. in / not in test containment, case-sensitively.s[i] indexes a single character, 0-based from the left or -1-based from the right.s[i:j:step] slices a range of characters, exactly like range(): start inclusive, end exclusive.s[::-1] reverses a string in one step. Wrap it in a helper function for clarity.
Behind every character is an integer code. This section covers len(), repr(), and the pair that connects characters to their codes: ord() and chr().
len(s) returns the number of characters in s. An escape sequence like \t is typed as two symbols but is a single character, so it only counts once.
print(len('cat\tdog')) # 7
Laid out character by character, 'cat\tdog' has exactly seven cells. The tab takes up one of them, the same as any letter.
'''
hi
'''
Run it and see whether len(s) matches what you predicted.
s = '''
hi
'''
print(len(s))
repr(s) returns a computer-readable form of s: quoted, with escape sequences shown as literal text instead of acted on. It's the tool for telling whether a string has hidden whitespace that print() would otherwise render invisibly.
s = '\thi\n'
print(s) # tab, hi, then a blank line
print(repr(s)) # '\thi\n'
s = ' go\n'
print(repr(s))
' go\n', quotes and escape sequence shown as textgo, followed by a real blank linego
ord(s) takes a length = 1 string and returns its integer character code. chr(n) does the reverse: given the integer, it returns the one-character string. Python originally used the ASCII standard for English-keyboard characters, and later adopted Unicode, a superset covering every language.
print(ord('m')) # 109
print(ord('Z')) # 90
print(chr(55)) # '7'
Each letter's code comes from ord(). Notice how consecutive letters get consecutive codes.
ord() turns the letter into a number, ordinary arithmetic shifts it, and chr() turns the result back into a letter.
Unicode includes thousands of characters with no key on a standard keyboard. Lists of them are usually given in hexadecimal (base 16, using digits 0–9 and A–F), written with a 0x prefix in Python.
star = chr(0x2605)
heart = chr(0x2764)
print(star, heart) # ★ ❤
Loop through a range of hex codes and print each one alongside the character it produces, including chr(0x2603). Then try a different range of your own.
Write shiftLetter(letter, shift), which takes a single uppercase letter and returns the letter shift positions later in the alphabet. Use ord() to get the letter's code, add shift, then use chr() to convert back. None of the test cases cross past 'Z', so you don't need to handle wraparound yet.
len(s) counts characters, including escape-sequence characters like \t and \n as one each.repr(s) shows a string's exact contents, escape sequences and all, which print() would otherwise hide.ord(s) turns a single character into its integer code; chr(n) turns a code back into a character.0x in Python.A method is a function attached to a specific value, called with a dot instead of parentheses around the value. String methods let a string test itself, edit itself, or search itself.
s.upper() calls the upper() method on s. If upper() were an ordinary function, we'd write upper(s) instead. Because upper(s) only works on strings, we call it a string method.
s = 'loud'
print(s.upper()) # LOUD
s.islower() / s.isupper(): are all the letters lower/uppercase?s.isalpha(): are all characters letters?s.isdigit(): are all characters digits?s.isspace(): are all characters whitespace?print('Ticket42'.isalpha()) # False
print('PASSWORD'.isupper()) # True
print('2024'.isdigit()) # True
Predict True or False for each: .islower(), .isupper(), .isalpha(), .isdigit(), all called on 'Pa55word'.
Run it and see how many of your four True/False predictions for 'Pa55word' were right.
s = 'Pa55word'
print(s.islower())
print(s.isupper())
print(s.isalpha())
print(s.isdigit())
s.lower() / s.upper(): a new string with every letter's case flipped.s.replace(old, new): a new string with every occurrence of old swapped for new.s.strip(): a new string with leading and trailing whitespace removed. Whitespace inside the string is untouched.print('Loud Noises!'.replace('Noises', 'Sounds'))
s = ' quiet please '
print(repr(s.strip())) # 'quiet please'
s = 'I like tea'
s.replace('tea', 'coffee')
print(s)
I like teaI like coffeeNone
Run it and see whether s still says tea.
s = 'I like tea'
s.replace('tea', 'coffee')
print(s)
s.count(t): how many times t occurs in s.s.startswith(t) / s.endswith(t): does s begin/end with t?s.find(t): the index of t's first occurrence, or -1 if absent.s.index(t): the same as find, but crashes instead of returning -1. Prefer find.s = 'Mississippi'
print(s.count('ss')) # 2
print(s.startswith('Miss')) # True
print(s.find('zz')) # -1
For which value of s do s.find('q') and s.index('q') behave differently?
s = 'quick's = 'queue's = 'slow'find and index always behave the same
Run this as-is first, then change message to a sentence of your own and see how each result changes.
s.method() calls a method on the value s, distinct from an ordinary function(s) call..isalpha(), .isdigit(), and friends) answer a yes/no question about a string's characters..count(), .find(), .index()) locate a substring. Prefer .find(), since it returns -1 instead of crashing when nothing is found..upper(), .replace(), .strip()) always return a new string. Strings can't be changed in place.Loops and strings pair naturally: a string is a sequence of characters, and a loop is a tool for visiting a sequence one element at a time. There are two ways to loop over a string, plus a method that splits one string into many.
When you want to process each character in a string, loop directly over the string. The loop variable becomes each character in turn.
s = 'CODE'
for c in s:
print(c)
count only grows when c is one of 'AEIOU'. Watch it hold steady on the consonant passes.
range(len(s)) produces every legal index into s: 0 up to len(s) - 1.i is an index, and s[i] reaches the character at that index.s = 'CODE'
for i in range(len(s)):
print(i, s[i])
Watch i climb from 0 to 3, and s[i] pick out the matching character each pass.
You need to print each character of a string together with its position, counting positions starting at 1 instead of 0. Which loop form fits?
for c in s:, since you're printing charactersfor i in range(len(s)):, then print(i + 1, s[i])
s.split(sep) breaks s apart everywhere sep occurs, returning the pieces to loop over. The loop variable is always a string, so numeric pieces need int() or float() to convert.
data = 'Ann,88,92,79'
for item in data.split(','):
print(item)
Each item starts as a string, so int(item) converts it before adding to the running total.
data now mixes words and numbers, so this crashes trying to int() a word. Add a check so the loop only adds up the numeric pieces. item.isdigit() can tell you which pieces those are.
split() takes whatever separator string you give it. Dates, phone numbers, and file paths are all "delimited data" once you know what to split on.
date = '2024-07-30'
for part in date.split('-'):
print(part) # 2024, then 07, then 30
s.splitlines() breaks a multiline string into its individual lines, without a trailing empty line even if s ends in a newline. Combine it with split() to loop over multiline, delimited data one row at a time.
roster = '''\
Ann,88,92
Ben,75,81
Cy,95,89
'''
for line in roster.splitlines():
print(line)
For each line of roster, this splits on commas, pulls the name from the first piece, and averages the rest as scores. Predict all three printed lines, then run it. Try adding a fourth student to roster.
data = '''\
x,y
z,w
'''
for row in data.splitlines():
for item in row.split(','):
print(item)
x, y, z, w: four lines, nothing elsex, y, z, w, then one extra blank linex,y then z,w: two lines, commas keptx, y, z, wfor i in range(len(s)): gives you both the index and, via s[i], the character.for c in s: is cleaner whenever you don't need the index, just each character.s.split(sep) breaks delimited data apart; s.splitlines() breaks a multiline string into rows. Combine both to loop over multiline delimited data.
Building by concatenating strings with + and including variables works, but it's easy to misplace a space or a quote or forget to convert the variable to a string.
An f-string is a far more direct way to weave values into text.
Putting f right before a string's opening quote makes it an f-string. Anywhere {variable} appears inside, Python substitutes that variable's current value. The f itself is not part of the string, it's a signal to Python.
Adding an equals sign, as in {battery = }, is a shortcut for quick debugging: Python prints the variable's name and its value together.
robot = 'R2D2'
battery = 87
s = f'{robot} is at {battery = }%'
By the time line 3 runs, both values already exist. The debug marker {battery = } is replaced with its own label and value, right inside the string.
The braces in an f-string aren't limited to a bare variable name. Any expression works: arithmetic, a method call, even both together. Python evaluates it and substitutes the result.
name = 'ada'
score = 88
bonus = 5
print(f'{name.upper()}: {score + bonus}')
An f-string is still a string literal underneath, so the same quoting rules apply. If the surrounding text has an apostrophe, switch the f-string's outer quotes to double, exactly as with any other literal.
marco = 'Marco'
food = 'tacos'
print(f"{marco}'s favorite food is {food}.")
name = 'Zoe', food = 'ramen'. Which line prints: Zoe's favorite food is ramen.
print(f"{name}'s favorite food is {food}.")print(f'{name}'s favorite food is {food}.')print(f"name's favorite food is food.")print(f'name's favorite food is food.')
This combines a method call, arithmetic, and an f-string in one line. The second print shows the debugging shortcut from earlier: wrapping an expression in parentheses and adding = prints its source text alongside its value. Run it, then try your own item, price, and quantity.
f right before the opening quote turns a literal into an f-string.{expression} inside an f-string is evaluated and substituted, whether it's a plain variable, arithmetic, or a method call. Wrapping it in parentheses and adding an equals sign, like {(price * qty)=}, can make debugging easier: it prints the expression's own source text alongside its value.\n escapes one into any string.+, *, in, indexing, and slicing all work on strings, indexing and slicing exactly like range()..upper() and .find() always return new values; strings never change in place.
To shift a letter by n, use the letter n positions later in the alphabet: 'a' shifted by 3 is 'd'. Once you reach the end of the alphabet, wrap back around to the beginning: 'z' shifted by 1 is 'a'. A Caesar Cipher shifts every letter in a message by the same amount, leaving non-letters unmodified and preserving each letter's case. A Caesar Cipher on 'I like zoos!' with a shift of 2 returns 'K nkmg bqqu!'.
Write encodeCaesarCipher(msg, shift), which performs a Caesar Cipher on msg, shifting each letter by shift characters. shift may be negative. Then write decodeCaesarCipher(encodedMsg, shift), which reverses a message that was encoded with that same shift. Once encodeCaesarCipher is working, decodeCaesarCipher is only a couple lines: what shift undoes a shift of shift?
For a letter c, find its position in its own case's alphabet with ord(c) - ord('A') or ord(c) - ord('a'), add shift, wrap with % 26, then convert back with chr(). Leave any character that isn't a letter untouched.
letterIndex %= 26 does not work correctly in this runner. Write it out as letterIndex = letterIndex % 26 instead.
topScorer(data) takes a multiline string of competition scores, one player per line. The first comma-separated value on a line is that player's name, guaranteed not to contain any digits. Every value after it is one non-negative score, and a player's total is the sum of every score on their line.
Return the name of the player with the highest total. If two or more players tie for the highest total, return their names as one comma-separated string, in the order they appeared in data. If data has no players at all, return the actual value None, not the string 'None'.
data.splitlines() gives you one row per player. Split each row on commas to separate the name from the scores, and track the running leader (or leaders) as you go.
Write the function isPalindrome(s) that returns True if s reads the same forwards and backwards, and False otherwise. Keep it case-sensitive: an uppercase letter never matches a lowercase one.
A single slicing trick from earlier in this deck solves the whole problem in one line.
Write countVowels(s) that returns the number of vowels (a, e, i, o, u) in s, counted case-insensitively so both cases count. y is never counted as a vowel here.
Write capitalizeWords(s) that returns s with the first letter of every word capitalized, leaving the rest of each word exactly as it was. Split s into words, rebuild each word as its capitalized first letter plus the remainder, and join the words back together with spaces.
s.split() with no argument splits on whitespace and never produces empty pieces, which keeps word[0] safe to index.
A pangram is a sentence that uses every letter of the alphabet at least once. Write isPangram(s) that returns True if s is a pangram, case-insensitively, and False otherwise.
Loop over string.ascii_lowercase, and for each letter check whether it's in s.lower().